(function() {
	var tokenRegex = /\{([^\}]+)\}/g, objNotationRegex = /(?:(?:^|\.)(.+?)(?=\[|\.|$|\()|\[('|")(.+?)\2\])(\(\))?/g, // matches .xxxxx or ["xxxxx"] to run over object properties
	replacer = function(all, key, obj) {
		var res = obj;
		key.replace(objNotationRegex, function(all, name, quote, quotedName,
				isFunc) {
			name = name || quotedName;
			if (res) {
				if (name in res) {
					res = res[name];
				}
				typeof res == "function" && isFunc && (res = res());
			}
		});
		res = (res == null || res == obj ? all : res) + "";
		return res;
	}, fill = function(str, obj) {
		return String(str).replace(tokenRegex, function(all, key) {
			return replacer(all, key, obj);
		});
	};
	Raphael.fn.popup = function(X, Y, set, pos, ret) {
		pos = String(pos || "top-middle").split("-");
		pos[1] = pos[1] || "middle";
		var r = 5, bb = set.getBBox(), w = Math.round(bb.width), h = Math
				.round(bb.height), x = Math.round(bb.x) - r, y = Math
				.round(bb.y)
				- r, gap = Math.min(h / 2, w / 2, 10), shapes = {
			top : "M{x},{y}h{w4},{w4},{w4},{w4}a{r},{r},0,0,1,{r},{r}v{h4},{h4},{h4},{h4}a{r},{r},0,0,1,-{r},{r}l-{right},0-{gap},{gap}-{gap}-{gap}-{left},0a{r},{r},0,0,1-{r}-{r}v-{h4}-{h4}-{h4}-{h4}a{r},{r},0,0,1,{r}-{r}z",
			bottom : "M{x},{y}l{left},0,{gap}-{gap},{gap},{gap},{right},0a{r},{r},0,0,1,{r},{r}v{h4},{h4},{h4},{h4}a{r},{r},0,0,1,-{r},{r}h-{w4}-{w4}-{w4}-{w4}a{r},{r},0,0,1-{r}-{r}v-{h4}-{h4}-{h4}-{h4}a{r},{r},0,0,1,{r}-{r}z",
			right : "M{x},{y}h{w4},{w4},{w4},{w4}a{r},{r},0,0,1,{r},{r}v{h4},{h4},{h4},{h4}a{r},{r},0,0,1,-{r},{r}h-{w4}-{w4}-{w4}-{w4}a{r},{r},0,0,1-{r}-{r}l0-{bottom}-{gap}-{gap},{gap}-{gap},0-{top}a{r},{r},0,0,1,{r}-{r}z",
			left : "M{x},{y}h{w4},{w4},{w4},{w4}a{r},{r},0,0,1,{r},{r}l0,{top},{gap},{gap}-{gap},{gap},0,{bottom}a{r},{r},0,0,1,-{r},{r}h-{w4}-{w4}-{w4}-{w4}a{r},{r},0,0,1-{r}-{r}v-{h4}-{h4}-{h4}-{h4}a{r},{r},0,0,1,{r}-{r}z"
		}, offset = {
			hx0 : X - (x + r + w - gap * 2),
			hx1 : X - (x + r + w / 2 - gap),
			hx2 : X - (x + r + gap),
			vhy : Y - (y + r + h + r + gap),
			"^hy" : Y - (y - gap)

		}, mask = [ {
			x : x + r,
			y : y,
			w : w,
			w4 : w / 4,
			h4 : h / 4,
			right : 0,
			left : w - gap * 2,
			bottom : 0,
			top : h - gap * 2,
			r : r,
			h : h,
			gap : gap
		}, {
			x : x + r,
			y : y,
			w : w,
			w4 : w / 4,
			h4 : h / 4,
			left : w / 2 - gap,
			right : w / 2 - gap,
			top : h / 2 - gap,
			bottom : h / 2 - gap,
			r : r,
			h : h,
			gap : gap
		}, {
			x : x + r,
			y : y,
			w : w,
			w4 : w / 4,
			h4 : h / 4,
			left : 0,
			right : w - gap * 2,
			top : 0,
			bottom : h - gap * 2,
			r : r,
			h : h,
			gap : gap
		} ][pos[1] == "middle" ? 1 : (pos[1] == "top" || pos[1] == "left") * 2];
		var dx = 0, dy = 0, out = this.path(fill(shapes[pos[0]], mask))
				.insertBefore(set);
		switch (pos[0]) {
		case "top":
			dx = X - (x + r + mask.left + gap);
			dy = Y - (y + r + h + r + gap);
			break;
		case "bottom":
			dx = X - (x + r + mask.left + gap);
			dy = Y - (y - gap);
			break;
		case "left":
			dx = X - (x + r + w + r + gap);
			dy = Y - (y + r + mask.top + gap);
			break;
		case "right":
			dx = X - (x - gap);
			dy = Y - (y + r + mask.top + gap);
			break;
		}
		out.translate(dx, dy);
		if (ret) {
			ret = out.attr("path");
			out.remove();
			return {
				path : ret,
				dx : dx,
				dy : dy
			};
		}
		set.translate(dx, dy);
		return out;
	};
})();